﻿#define _CRT_SECURE_NO_WARNINGS 1
#include"snack.h"
void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE hOutput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值) 
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos 
	SetConsoleCursorPosition(hOutput, pos);
}
void InitConsole()
{
	//将控制台主机界面大小改为100列，35行：
	system("mode con cols=100 lines=35");
	//将控制台更名为“贪吃蛇”：
	system("title 贪吃蛇");
}
void WelcomeScreen()
{
	SetPos(40, 13);
	wprintf(L"欢迎来到贪吃蛇小游戏！\n");
	SetPos(40, 15);
	system("pause");
	system("cls");
}
void ShowGameInstructions()
{
	SetPos(7, 10);
	wprintf(L"在接下来的游戏中，■代表墙面，●代表蛇，★代表食物，蛇一但撞到墙或自己，游戏就会结束\n");
	SetPos(40, 12);
	system("pause");
	system("cls");
}
//创建蛇
pSnackNode Snack()
{
	//使用链表创建蛇
	pSnackNode psnack = NULL;
	for (int i = 0; i < 6; ++i)
	{
		pSnackNode cur = (pSnackNode)malloc(sizeof(SnackNode));
		assert(cur);
		cur->_x = START_X + 10 + 2 * i;//注意蛇身的每个图形的边都要相对墙来说占据奇数位置
		cur->_y = START_Y + 6;
		cur->next = NULL;
		if (psnack == NULL)//当psnack（蛇头）为空时，将cur赋值给蛇头
		{
			psnack = cur;
		}
		else {//当蛇头非空时，通过将cur->next指向蛇头然后，蛇头赋值为新cur来实现头插
			cur->next = psnack;
			psnack = cur;
		}
	}
	return psnack;
}
//判断一个坐标是否为蛇
bool IsSnack(int x, int y, pSnackNode pheadsnack)
{
	pSnackNode cur = pheadsnack;
	while (cur != NULL)
	{
		if (cur->_x == x && cur->_y == y)
		{
			return true;
		}
		cur = cur->next;
	}
	return false;
}
//判断一个坐标是否为蛇尾
bool IsSnackTail(int x, int y, pSnackNode pheadsnack)
{
	pSnackNode cur = pheadsnack;
	while (cur->next != NULL)
	{
		cur = cur->next;
	}
	return cur->_x == x && cur->_y == y;
}
//创建食物
pSnackNode Food(pSnackNode pheadsnack)
{
	//由于食物节点中也包含横纵坐标，且蛇后续吃食物可以通过连接链表的方式来进行
	//索性将食物节点设置为了SnackNode类型
	pSnackNode pfood = (pSnackNode)malloc(sizeof(SnackNode));
	assert(pfood);
	do {
		//使用随机数来实现随机位置生成食物：注意食物的位置也要在相对墙的奇数位置
		pfood->_x = rand() % (COLS - 5) + 2 + START_X;//这里坐标的逻辑稍复杂，建议画图来理解
		if (((COLS - 6 + 2 + START_X) - pfood->_x) % 2 == 1)//这里算出_x的最大合理值（即_x的相对墙最大奇数位置）与_x的差值，若%2为零，则为合理值，否则为相对墙的偶数位置（以下称为错误值）
		{
			++pfood->_x;//但由于%运算符的存在，错误值不可能大于最大合理值，或者说，最大错误值为最大合力值-1，也就是说，出现错误值时，只需要+1就能保证其合理，其不会大于最大合理值
		}
		pfood->_y = rand() % (ROWS - 2) + 1 + START_Y;
		pfood->next = NULL;
	} while (IsSnack(pfood->_x, pfood->_y, pheadsnack));
	return pfood;
	//这里其实有可能发生蛇占满了整个棋盘，导致Food函数死循环的bug
	//解决方法很好想到，可以在游戏的结构体中多封装一个蛇长度信息
	//在该循环中进行蛇长度==棋盘中原空格数量的判断并进行相应处理即可
	//但由于发生这种情况的概率极低，这游戏也没人玩，这里就不做处理了
}
void PrintScore(pGame_Snack pgame_snack)
{
	SetPos(START_X + 60, 16);
	wprintf(L"当前食物分数：%2d", pgame_snack->food_score);
	SetPos(START_X + 60, 17);
	wprintf(L"目前已取得的总分：%2d", pgame_snack->total_​score);
}
void PrintFood(pGame_Snack pgame_snack)
{
	SetPos(pgame_snack->pfood->_x, pgame_snack->pfood->_y);
	wprintf(L"★");
}
void PrintSnack(pGame_Snack pgame_snack)
{
	pSnackNode cur = pgame_snack->pheadsnack;
	while (cur != NULL)
	{
		SetPos(cur->_x, cur->_y);
		wprintf(L"●");
		cur = cur->next;
	}
}
//判断该坐标是否为墙
bool IsWall(int x, int y)
{
	return x == START_X || x == START_X + COLS - 2 || y == START_Y || y == START_Y + ROWS - 1;
}
//判断该坐标是否为食物
bool IsFood(int x, int y, pSnackNode pfood)
{
	return x == pfood->_x && y == pfood->_y;
}
//输出蛇移动的下一个节点
pSnackNode NextSnackNode(pGame_Snack pgame_snack)
{
	pSnackNode next_snack_node = (pSnackNode)malloc(sizeof(SnackNode));
	assert(next_snack_node);
	switch (pgame_snack->dir)
	{
	case UP:
		next_snack_node->_x = pgame_snack->pheadsnack->_x;
		next_snack_node->_y = pgame_snack->pheadsnack->_y - 1;
		break;
	case DOWN:
		next_snack_node->_x = pgame_snack->pheadsnack->_x;
		next_snack_node->_y = pgame_snack->pheadsnack->_y + 1;
		break;
	case LEFT://这里需注意左右移动一次为x-2 / x+2
		next_snack_node->_x = pgame_snack->pheadsnack->_x - 2;
		next_snack_node->_y = pgame_snack->pheadsnack->_y;
		break;
	case RIGHT:
		next_snack_node->_x = pgame_snack->pheadsnack->_x + 2;
		next_snack_node->_y = pgame_snack->pheadsnack->_y;
		break;
	}
	return next_snack_node;
}
void NormalPrint(pGame_Snack pgame_snack, pSnackNode next_snack_node)
{
	next_snack_node->next = pgame_snack->pheadsnack;
	pgame_snack->pheadsnack = next_snack_node;
	pSnackNode cur = pgame_snack->pheadsnack;
	while (cur->next->next != NULL)//由于蛇身长度初始化时就设置为6，所以不用担心cur->next->next会造成解引用空指针
	{
		cur = cur->next;
	}
	SetPos(cur->next->_x, cur->next->_y);
	printf("  ");//如果下一个位置什么都没有，就要把此时的蛇尾覆盖为空格
	free(cur->next);
	cur->next = NULL;
	PrintSnack(pgame_snack);
}
void Stop()
{
	//提示“游戏暂停中……”字样
	SetPos(START_X + COLS / 2 - 6, START_Y + ROWS / 2 - 2);//定位光标，将字样显示在显眼的位置
	printf("游戏暂停中……");
	while (1)
	{
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		Sleep(100);
	}
	//使用空格进行覆盖，以清除“游戏暂停中……”字样
	SetPos(START_X + COLS / 2 - 6, START_Y + ROWS / 2 - 2);
	printf("              ");
}
void GameScreen(pGame_Snack pgame_snack)
{
	//绘制游戏界面的围墙与游戏说明
	//打印墙的上边
	SetPos(START_X, START_Y);
	for (int i = 0; i < COLS / 2; ++i)
	{
		wprintf(L"■");
		SetPos(START_X + i * 2, START_Y);
	}
	//打印墙的下边
	SetPos(START_X, START_Y + ROWS - 1);
	for (int i = 0; i < COLS / 2; ++i)
	{
		wprintf(L"■");
		SetPos(START_X + i * 2, START_Y + ROWS - 1);
	}
	//打印墙的左边
	SetPos(START_X, START_Y);
	for (int i = 0; i < ROWS; ++i)
	{
		wprintf(L"■");
		SetPos(START_X, START_Y + i);
	}
	//打印墙的右边
	SetPos(START_X + COLS - 2, START_Y);
	for (int i = 0; i < ROWS + 1; ++i)//ROWS+1是因为正常打印ROWS似乎会覆盖最后一个■，因此，多打印一次
	{
		wprintf(L"■");
		SetPos(START_X + COLS - 2, START_Y + i);
	}
	
	//打印游戏说明：
	SetPos(START_X + 60, 10);
	wprintf(L"可使用↑.↓.← .→ 分别控制蛇的移动，");
	SetPos(START_X + 60, 11);
	wprintf(L"A为加速，B为减速，且速度越快，食物的");
	SetPos(START_X + 60, 12);
	wprintf(L"分数越高，并且，按下空格键可以暂停游");
	SetPos(START_X + 60, 13);
	wprintf(L"戏，但需要注意，不能让蛇撞到墙或自己");
	SetPos(START_X + 60, 14);
	wprintf(L"，这会使得游戏结束");
	//打印分数
	PrintScore(pgame_snack);
	//在游戏界面中打印蛇和食物
	PrintSnack(pgame_snack);//这里尝试先将蛇的初始状态打印出来
	while (pgame_snack->game_status == RUN_NORMAL)
	{
		//打印食物
		if (pgame_snack->pfood == NULL)
		{
			pgame_snack->pfood = Food(pgame_snack->pheadsnack);
			PrintFood(pgame_snack);
			PrintScore(pgame_snack);
		}
		else {
			PrintFood(pgame_snack);//就算重复打印食物，也不会造成过大损耗
		}

		//打印蛇
		//蛇移动时可以创建一个SnackNode节点，将其坐标根据移动方向设置为蛇头的下一个位置
		//然后看该节点是否为食物/墙/蛇身，若均不是，则可以直接将新节点设置为蛇头，并将蛇的最后一个节点删除
		//最后将被删除节点的位置打印为空格，若下一个位置为食物/墙/蛇身，就要分情况讨论了
		pSnackNode next_snack_node = NextSnackNode(pgame_snack);
		if (IsWall(next_snack_node->_x, next_snack_node->_y))
		{
			pgame_snack->game_status = KILL_BY_WALL;
			NormalPrint(pgame_snack, next_snack_node);//最好将撞墙那一步打印出来再结束循环
		}
		else if (IsFood(next_snack_node->_x, next_snack_node->_y, pgame_snack->pfood))
		{
			next_snack_node->next = pgame_snack->pheadsnack;
			pgame_snack->pheadsnack = next_snack_node;
			pgame_snack->total_​score += pgame_snack->food_score;
			pgame_snack->pfood = NULL;//吃完记得将食物指针置空，在下次循环时检测到为NULL会重新生成食物
			PrintSnack(pgame_snack);
			
		}
		else if (IsSnack(next_snack_node->_x, next_snack_node->_y, pgame_snack->pheadsnack) && !IsSnackTail(next_snack_node->_x, next_snack_node->_y, pgame_snack->pheadsnack))
		{
			pgame_snack->game_status = KILL_BY_SELF;
			NormalPrint(pgame_snack, next_snack_node);//最好将撞到自己那一步打印出来再结束循环
		}
		else {
			NormalPrint(pgame_snack, next_snack_node);
		}
		Sleep(pgame_snack->sleep_time);//注意这里sleep的时机，打印后立即sleep用户可以反应过来按下按键，在此时进行即刻转向，实际效果就是显得按键很灵敏
									   //但如果将sleep加在检测按键后，会导致打印的一瞬间就进行按键检测，想要调整位置就需要抓住打印的一瞬间，否则就要下次打印才能转向了

		//检测按键情况，对蛇的方向进行调整
  		if (KEY_PRESS(VK_UP) && pgame_snack->dir != DOWN)
		{
			pgame_snack->dir = UP;
		}
 		else if (KEY_PRESS(VK_DOWN) && pgame_snack->dir != UP)
		{
			pgame_snack->dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && pgame_snack->dir != RIGHT)
		{
			pgame_snack->dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && pgame_snack->dir != LEFT)
		{
			pgame_snack->dir = RIGHT;
		}

		//检测按键情况，实现蛇的变速/游戏暂停等功能
		if (KEY_PRESS(0x41) && pgame_snack->sleep_time > 20)//似乎vs2022没有VK_A，但可以使用0x41代表键A
		{
			pgame_snack->sleep_time -= 30;
			if (pgame_snack->sleep_time == 10)//当速度为time==10时，食物分数直接涨到25
			{
				pgame_snack->food_score = 25;
			}
			else {
				pgame_snack->food_score += 1;
			}
			PrintScore(pgame_snack);
		}
		else if (KEY_PRESS(0x44) && pgame_snack->sleep_time < 370)
		{
			pgame_snack->sleep_time += 30;
			if (pgame_snack->food_score == 25)
			{
				pgame_snack->food_score = 12;
			}
			else {
				pgame_snack->food_score -= 1;
			}
			PrintScore(pgame_snack);
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Stop();
		}
 	}
	if (pgame_snack->game_status == KILL_BY_SELF)
	{
		SetPos(START_X + COLS / 2 - 6, START_Y + ROWS / 2 - 2);
		printf("您撞到了自己，游戏结束！");
	}
	else if (pgame_snack->game_status == KILL_BY_WALL)
	{
		SetPos(START_X + COLS / 2 - 6, START_Y + ROWS / 2 - 2);
		printf("您撞到了墙，游戏结束！");
	}
	SetPos(START_X + COLS / 2 - 6, START_Y + ROWS / 2 - 1);
	printf("您的最终得分为：%d", pgame_snack->total_​score);
}

